Перейти к основному содержимому

5.15. Типы данных и переменные

Разработчику Архитектору

Типы данных и переменные

Динамическая типизация
типы данных в Lua
Особенности:
Все числа — double (в классическом Lua)
Нет отдельного типа для целых/плавающих

Переменные
Виды переменных
Объявление переменных
Преобразования типов

Коллекции - таблицы, главная структура данных в Lua
Массивоподобные таблицы
Ассоциативные массивы (словари)
Смешанные ключи


Система типов в Lua построена на принципах динамической типизации и минимализма.

Динамическая типизация подразумевает, что тип значения ассоциируется не с переменной, а со значением. Технически, переменная представляет собой ссылку (или имя) на объект определённого типа, при этом одна и та же переменная может в разные моменты времени ссылаться на значения различных типов. Это позволяет писать гибкий и лаконичный код, но требует от разработчика повышенной осмотрительности в управлении состоянием программы.

То есть, переменная может быть сначала одного типа, а затем другого:

local x = 42        -- x ссылается на значение типа number
x = "hello" -- теперь x ссылается на значение типа string
x = true -- теперь x — булево значение

Подобное поведение исключает необходимость явного объявления типов переменных и обеспечивает высокую степень метапрограммирования, однако делает статический анализ типов невозможным без использования внешних инструментов (например, Lua LSP или MoonScript).

В Lua определено восемь базовых типов данных, каждый из которых представляет собой самостоятельную категорию значений. Они могут быть классифицированы следующим образом:

  1. nil — тип, имеющий единственный экземпляр nil. Используется для обозначения отсутствия значения. Переменная, которой не присвоено значение, по умолчанию содержит nil. Это аналог null в других языках.

Такое определение может сильно запутать. Чтобы понять, нужно запомнить - в Lua нет «неинициализированных переменных» в классическом смысле, и если переменной ничего не присвоено, она просто содержит nil - то есть, «ничего».

Соответственно, у типа nil есть только одно возможное значение — сам nil. Любое выражение, результат которого «ничего», возвращает nil. В условиях nil считается ложным, вместе с false — это единственные два ложных значения в Lua. Всё остальное (включая 0, пустую строку "", пустую таблицу ) — истинно. И если вы объявите переменную:

local x
print(x)

То результат функции print(x) будет nil, то есть «ничего».

nil можно использовать для удаления элементов из таблиц:

local t = {a = 1, b = 2}
t.a = nil -- удаляем ключ 'a'
print(t.a) --> nil

Вы наверное задались вопросом - о каких таблицах идёт речь? Запомните - ещё вернёмся. Давайте сначала разберём прочие виды данных.

  1. boolean — логический тип, допускающий два значения: true и false. В контексте условий любое значение, кроме nil и false, интерпретируется как истинное.
local active = true
local paused = false

if active and not paused then
print("Работает")
end

Здесь мы видим переменные active и paused. Обратите внимание на проверку - она выполняется словами. В большинстве языков мы бы написали if active && !paused, используя символ && как «и», или «and», а также символ «!» как отрицание. Здесь же мы используем именно слова «and» и «not».

  1. number — числовой тип. В классическом Lua (5.1–5.4) все числа представлены как 64-битные числа с плавающей запятой двойной точности (double) в соответствии со стандартом IEEE 754. Отдельного типа для целых чисел (integer) не существует, хотя начиная с Lua 5.3 была добавлена возможность использования целочисленного подтипа внутри типа number, который автоматически переключается между целыми и вещественными представлениями в зависимости от операции.

  2. string — неизменяемые последовательности байтов, используемые для представления текста. Строки в Lua не имеют ограничений на содержание нулевых байтов и могут хранить произвольные бинарные данные. Интернирование строк позволяет эффективно сравнивать их по ссылке.

local name = "Иван"
local message = [[
Многострочная строка.
Можно использовать любые символы, включая "кавычки".
]]
print(#message) --> длина строки
  1. function — тип, представляющий вызываемые объекты. Функции являются объектами первого класса: их можно передавать как аргументы, возвращать из других функций, хранить в переменных и структурах данных.
local add = function(a, b)
return a + b
end

print(add(3, 4)) --> 7

-- Функции — объекты первого класса:
local ops = {add = add}
print(ops.add(2, 3)) --> 5

Но о функциях мы будем говорить отдельно.

  1. table — единственная составная структура данных в Lua. Таблицы реализуют ассоциативные массивы и служат основой для построения массивов, словарей, объектов, модулей и других абстракций. Подробнее рассматривается ниже.
local user = {
name = "Bob",
age = 25,
hobbies = {"chess", "coding"}
}

print(user.name) --> Bob
print(user.hobbies[1]) --> chess

Здесь таблицей является user. Таблица является универсальным контейнером. В большинстве объектно-ориентированных языков такой тип данных называется объектом, но в Lua это таблицы.

  1. userdata — тип, предназначенный для хранения произвольных данных, определяемых C-кодом. Используется преимущественно при расширении Lua через C API. Существует два вида: light userdata (указатель на C-объект) и full userdata (выделенная память, управляемая сборщиком мусора).

Используется при интеграции с C (Си). К примеру, это может быть хранение указателя на C-структуру:

// В C-расширении:
lua_pushlightuserdata(L, my_pointer);

-- В Lua:
local ptr = lightuserdata -- например, из C API
-- Нельзя изменять из Lua, только передавать обратно в C
  1. thread — представляет поток выполнения (coroutine). Не следует путать с системными потоками; это легковесные сопрограммы, управляемые на уровне интерпретатора Lua.
local co = coroutine.create(function()
for i = 1, 3 do
print(i)
coroutine.yield()
end
end)

coroutine.resume(co) --> 1
coroutine.resume(co) --> 2
coroutine.resume(co) --> 3

Определить тип значения можно с помощью встроенной функции type(), которая возвращает строку с именем типа:

print(type(42))           --> number
print(type(nil)) --> nil
print(type(function() end)) --> function

Особое внимание заслуживает тип number. В Lua 5.1 и 5.2 все числовые значения реализованы исключительно как double-значения, что означает, что даже целые числа хранятся в формате с плавающей запятой. Это приводит к некоторым нетривиальным последствиям, таким как потеря точности при работе с очень большими целыми числами, невозможность различать целые и вещественные числа на уровне типа, и автоматическое округление в арифметических операциях, если результат не может быть точно представлен.

Начиная с Lua 5.3, была внедрена поддержка целочисленного внутреннего представления. Теперь тип number может хранить как 64-битные целые (int64), так и double-вещественные числа. Lua автоматически выбирает наиболее подходящее представление, а при необходимости выполняет преобразования.

Но это всё внутренняя оптимизация, с точки зрения языка number остаётся единым типом. Программист не может объявить переменную как «целое число», и проверка типа не различает подтипы.

Переменные в Lua не имеют фиксированного типа и не требуют явного объявления. Любая переменная создаётся в момент первого присваивания. По области видимости переменные делятся на три категории:

Локальные переменные — объявляются с помощью ключевого слова local. Имеют блочную область видимости и рекомендуются к использованию по умолчанию. Они доступны только внутри блока, где объявлены (будь то цикл, функция, условие). Если они выходят из области видимости, то уничтожаются сборщиком мусора автоматически.

local function greet()
local name = "Alice"
print(name) -- работает
end

greet()
print(name) -- ошибка: name is nil (локальная вне функции недоступна)

Здесь мы видим переменную с именем name - она и будет локальной, доступной только в пределах функции greet().

Глобальные переменные — создаются при первом присваивании без local. Хранятся в глобальной таблице _G. Использование глобальных переменных считается антипаттерном в крупных проектах.

Такие переменные доступны из любого места программы, и очевидно, что это загрязняет глобальное пространство имён, усложняя отладку, поэтому, если нет нужды, лучше создавать именно локальные переменные.

x = 100             -- глобальная переменная
_G.y = 200 -- то же самое, что y = 200

print(_G.x) --> 100

Для объявления глобальной переменной не нужно никаких ключевых слов, просто написать имя и присвоить значение через символ «=». Тип данных тоже писать не нужно.

Это отличный пример работы динамической неявной типизации - программисту не нужно явно указывать тип данных, за него всё сделает язык.

Управляющие переменные циклов — например, i в for i = 1, 10 do ... end — являются локальными по умолчанию.

Пример:

x = 10              -- глобальная переменная
local y = 20 -- локальная переменная

Отсутствие объявления типа и автоматическое создание глобальных переменных могут привести к ошибкам из-за опечаток. Поэтому в производственных средах рекомендуется использовать режим строгой проверки (например, через библиотеку strict.lua) или статические анализаторы.

Существует библиотека (или фрагмент кода) под названием strict.lua — это не часть стандартной библиотеки, а стороннее средство, используемое для включения строгой проверки использования переменных. Она работает через метатаблицы и отслеживание доступа к глобальной таблице _G. Если вы читаете или записываете в необъявленную глобальную переменную — выбрасывается ошибка. На практике такие вещи внедряют в крупные проекты (например, в играх на Love2D или в движках вроде Neovim), где важно контролировать состояние глобального пространства имён.

И как раз-таки рекомендуется использовать strict-режим, чтобы избежать случайных глобальных переменных.

Lua поддерживает автоматическое неявное преобразование между строками и числами в соответствующих контекстах. Это удобно, но требует осторожности.

При арифметических операциях строковые значения, содержащие корректные числовые литералы, автоматически преобразуются в числа:

print("10" + 1)     --> 11

При конкатенации (..) числа автоматически преобразуются в строки:

print(42 .. " is the answer") --> "42 is the answer"

Неудачные попытки преобразования приводят к ошибкам:

print("hello" + 1)    -- ошибка: attempt to perform arithmetic on a string value

Для явного преобразования используются функции:

  • tonumber(s) — преобразует строку в число; возвращает nil, если преобразование невозможно.
  • tostring(v) — преобразует значение любого типа в строку.

Пользовательские правила преобразования могут быть заданы через метатаблицы (например, метаметоды __tostring и __tonumber).

Центральным элементом системы типов Lua является тип table, являющийся единственным механизмом для создания составных данных. Таблица представляет собой ассоциативный массив, то есть отображение ключей на значения. Ключи и значения могут быть любого типа, кроме nil.

Таблицы часто используются как упорядоченные последовательности, аналогичные массивам в других языках. По соглашению, такие таблицы используют целочисленные ключи, начинающиеся с 1 (в отличие от языков с нулевой индексацией):

local arr = {"apple", "banana", "cherry"}
print(arr[1]) --> "apple"

Функция #arr возвращает длину последовательности (количество элементов до первого nil). Однако необходимо помнить, что Lua не гарантирует плотность массива, и пропущенные индексы могут нарушить работу оператора длины.

Оператор # (или функция table.getn() в старых версиях) пытается определить длину «последовательности» — то есть количество элементов, идущих подряд с индекса 1. Но только до первого nil.

«Нормальный» массив:

local arr = {10, 20, 30}
print(#arr) --> 3

Массив с «дырой»:

local arr = {10, 20, nil, 40}
print(#arr) --> 2 (!!!)

То есть, элемент 40 игнорируется, потому что между 2 и 4 есть nil, и на третьем шаге будет конец последовательности.

Таблицы реализованы как хэш-таблицы с возможностью хранения упорядоченной последовательности (массивная часть) и хэшированной части (ассоциативная). Это позволяет эффективно комбинировать доступ по индексу и по ключу. Производительность таблиц зависит от равномерности распределения хэшей и размера структуры.

Упорядоченная последовательность - это таблица, используемая как массив: ключи — целые числа, начинающиеся с 1, идут подряд.

local fruits = {"apple", "banana", "orange"}
-- эквивалентно:
-- fruits[1] = "apple"
-- fruits[2] = "banana"
-- fruits[3] = "orange"

Здесь важна упорядоченность и непрерывность индексов.

Ассоциативные массивы (словари) являются структурой, где каждому значению соответствует ключ, и доступ к данным осуществляется по ключу, а не по позиции. В Lua любая таблица может быть ассоциативным массивом.

Таблицы одинаково хорошо работают как словари (hash maps), где ключами могут быть строки, числа или даже другие таблицы:

local person = {
name = "Alice",
age = 30,
["city"] = "Moscow"
}
print(person["name"]) --> "Alice"
print(person.age) --> 30

Здесь синтаксис t.key эквивалентен t["key"], если ключ является допустимым идентификатором.

Смешанные ключи - важная часть, касающаяся словарей. В одной таблице можно использовать разные типы ключей одновременно. Ключи разных типов не конфликтуют.

Особенность таблиц — возможность смешивать различные типы ключей в одной структуре:

local t = {}
t[1] = "first"
t["hello"] = "world"
t[{}] = 42 -- ключ — анонимная таблица
t[true] = "yes"

Каждый ключ уникален: два разных объекта (например, две разные таблицы) никогда не считаются равными, даже если они содержат одинаковые данные.

Таблицы поддерживают механизм метатаблиц, позволяющий переопределять поведение операций (арифметика, сравнение, индексация и т. д.) через специальные метаметоды. Это делает таблицы основой для реализации ООП, операторной перегрузки и доменных DSL.

local mt = {
__add = function(a, b)
return setmetatable({value = a.value + b.value}, mt)
end
}
local a = setmetatable({value = 10}, mt)
local b = setmetatable({value = 20}, mt)
local c = a + b
print(c.value) --> 30